function [taskTree, taskList] = configureDots2afcTask
%Configure a dots kinetogram 2-alternative forced choice task
%
%   [taskTree, taskList] = configureDots2afcTask
%
%   taskTree is a topsTreeNode object which organizes tasks and trials.
%   taskTree.run() will start the task.  taskTree.gui() will launch a
%   graphical interface for viewing the organization of the task.
%
%   taskList is a topsGroupedList object which holds all the objects and
%   data needed to run the task, including taskTree.  taskList.gui() will
%   launch a graphical interface for viewing the objects and data.
%
%   The task uses graphical stimuli including targets, a fixation point,
%   and a random dot kinetogram.  The subject is expected to decide towards
%   which target the dots in the kinetogram appeared to be moving.
%
%   The task uses a HID gamepad to take input.  The subject may highlight
%   one target at a time using the gamepad's x and y axis controls.  The
%   subject may indicate a final decision by pressing the gamepad's primary
%   button.
%
%   The task contains multiple trials, each containing one stimulus
%   presentation and one decision.  Generally, the trials follow the same
%   sequence:
%       - the fixation point appears on the blank screen
%       - there is a "preparation" time interval
%       - the dots kinetogram appears on the screen
%       - there is a "stimulus" time interval:
%           - the kinetogram presents a motion signal
%           .
%       - the dots kinetogram is replaced on the screen by two gray targets
%       - there is a "choice" time interval:
%           - the subject may select a target
%           .
%       - the target matching the kinetogram's motion signal turns green,
%       the other target turns red
%       - there is a short "feedback" interval
%       - the screen goes blank
%       - the trial ends and an intertrial interval follows
%       .
%
%   There are three variations on this trial sequence, each with different
%   timing behavior.  The three trial types are:
%       - Fixed Time
%           - the "preparation" interval is constant
%           - the "stimulus" interval is constant
%           - the "choice" interval is constant
%           .
%       - Reaction Time
%           - the "preparation" interval is constant
%           - the "stimulus" interval lasts until the subject moves an axis
%           control on the gamepad
%           - the "choice" interval lasts until the subject presses the
%           primary button on the gamepad.
%           .
%       - Preparation Time
%           - the "preparation" interval lasts until the subject presses
%           the primary button on the gamepad.
%           - the "stimulus" interval is constant
%           - the "choice" interval is constant
%           .
%       .
%   The "feedback" interval is constant for all trial types.
%
%   Task stimulus and task data are logged with topsDataLog.  Data about
%   the flow of events during the task are also logged.  Following a run of
%   the 2afc task, try topsDataLog.gui() to view logged task events.
%
%   Task parameters, such as the lengths of constant time intervals, may be
%   edited near the top of this file, configureDots2afcTask.m.
%
% 2010 benjamin.heasly@gmail.com
%   Seattle, WA


%% Organization:
% a container for all task data and objects
%   partitioned into arbitrary groups
%   viewable with taskList.gui
taskList = topsGroupedList;

% give the task a name, used below
taskName = 'dots2afc';


%% Constants:
% store some constants in the taskList container
%   used during configuration
%   used while task is running
taskList{'timing'}{'preparation'} = 1;
taskList{'timing'}{'stimulus'} = 1;
taskList{'timing'}{'choice'} = 2;
taskList{'timing'}{'feedback'} = 1;
taskList{'timing'}{'trial timeout'} = 600;
taskList{'timing'}{'intertrial'} = .25;

taskList{'graphics'}{'white'} = [255 255 255];
taskList{'graphics'}{'gray'} = [128 128 128];
taskList{'graphics'}{'red'} = [196 64 32];
taskList{'graphics'}{'green'} = [64 196 32];
taskList{'graphics'}{'stimulus diameter'} = 5;
taskList{'graphics'}{'fixation pixels'} = 10;
taskList{'graphics'}{'leftward'} = 180;
taskList{'graphics'}{'rightward'} = 0;
taskList{'graphics'}{'left side'} = -5;
taskList{'graphics'}{'right side'} = 5;
taskList{'graphics'}{'neutral pixels'} = [5, 5];
taskList{'graphics'}{'leftward pixels'} = [30, 5];
taskList{'graphics'}{'rightward pixels'} = [5, 30];

taskList{'control'}{'blocks per session'} = 1;

%% Graphics:
% create some graphics objects
%   configure them using constants from above
%   store them in the taskList container
dm = dotsTheDrawablesManager.theObject;

% a fixation point
fp = dm.newObject('dotsDrawableTargets');
fp.color = taskList{'graphics'}{'gray'};
fp.dotSize = taskList{'graphics'}{'fixation pixels'};
taskList{'graphics'}{'fixation point'} = fp;

% target dots
targs = dm.newObject('dotsDrawableTargets');
targs.color = taskList{'graphics'}{'gray'};
targs.dotSize = taskList{'graphics'}{'neutral pixels'};
left = taskList{'graphics'}{'left side'};
right = taskList{'graphics'}{'right side'};
targs.x = [left, right];
targs.y = [0 0];
targs.isVisible = false;
taskList{'graphics'}{'targets'} = targs;

% a random dots stimulus
stim = dm.newObject('dotsDrawableDotKinetogram');
stim.color = taskList{'graphics'}{'white'};
stim.dotSize = 3;
stim.direction = 0;
stim.diameter = taskList{'graphics'}{'stimulus diameter'};
stim.isVisible = false;
taskList{'graphics'}{'stimulus'} = stim;

% add all objects to one group, to be drawn simultaneously
dm.addObjectToGroup(fp, taskName);
dm.addObjectToGroup(targs, taskName);
dm.addObjectToGroup(stim, taskName);
dm.activateGroup(taskName);

% use a name for the draw manager in the gui
dm.name = 'draw';

%% Input:
% create an input object, a gamepad
%   configure it to classify inputs as needed for this task
%   store it in the taskList container
qm = dotsTheQueryablesManager.theObject;
gp = qm.newObject('dotsQueryableHIDGamepad');
taskList{'input'}{'gamepad'} = gp;
if gp.isAvailable
    % identify axes movement phenomenons
    %   see gp.phenomenons.gui()
    left = gp.phenomenons{'axes'}{'min_X'};
    right = gp.phenomenons{'axes'}{'max_X'};
    
    % identify primary button press phenomenon
    %   see gp.phenomenons.gui()
    commit = gp.phenomenons{'pressed'}{'pressed_button_1'};
    pressTwo = gp.phenomenons{'pressed'}{'pressed_button_2'};
    quit = dotsPhenomenon.composite([commit, pressTwo], 'intersection');
    
    hid = gp;
else
    % fallback on keyboard inputs
    kb = qm.newObject('dotsQueryableHIDKeyboard');
    taskList{'input'}{'keyboard'} = kb;
    left = kb.phenomenons{'pressed'}{'pressed_KeyboardLeftArrow'};
    right = kb.phenomenons{'pressed'}{'pressed_KeyboardRightArrow'};
    commit = kb.phenomenons{'pressed'}{'pressed_KeyboardSpacebar'};
    quit = kb.phenomenons{'pressed'}{'pressed_KeyboardEscape'};
    
    hid = kb;
end
taskList{'input'}{'using'} = hid;
% classify phenomenons to produce arbitrary outputs
%   each output will match a state name, below
hid.addClassificationInGroupWithRank(left, 'leftward', taskName, 2);
hid.addClassificationInGroupWithRank(right, 'rightward', taskName, 3);
hid.addClassificationInGroupWithRank(commit, 'commitment', taskName, 4);
hid.addClassificationInGroupWithRank(quit, 'quit', taskName, 5);
hid.activeClassificationGroup = taskName;

%% Control:
% create three types of control objects:
%       - topsConditions organizes combinations of parameter values for
%       each trial
%       - topsTreeNode organizes flow outside of trials
%       - topsStateMachine organizes flow within trials
%       - topsCallList organizes batches of functions to be called together
%       - topsConcurrentComposite interleaves behaviors of the drawables
%       manager, state machine, and call list.
%   connect these to each other
%   store them in the taskList container

% combinations of parameter values to traverse during each trial type
%   and where to set the parameter values before each trial
taskConditions = topsConditions;
taskConditions.name = 'pick conditions';
taskConditions.addParameter('direction', {0, 180});
taskConditions.addAssignment('direction', stim, '.', 'direction');
taskConditions.setPickingMethod('shuffled', 2);
taskList{'control'}{'task conditions'} = taskConditions;

% the trunk of the tree, branches are added below
taskTree = topsTreeNode;
taskTree.name = '2afc task:';
taskTree.iterations = taskList{'control'}{'blocks per session'};
taskTree.startFevalable = {@openScreenWindow, dm};
taskTree.finishFevalable = {@closeScreenWindow, dm};

% a batch of function calls that apply to all the trial types below
%   start- and finishFevalable get called once per trial
%   any addCall fevalables get called repeatedly during a trial
trialCalls = topsCallList;
trialCalls.name = 'call functions';
trialCalls.alwaysRunning = true;
trialCalls.startFevalable = {@mayDrawNextFrame, dm, true};
trialCalls.addCall({@readData, hid});
trialCalls.finishFevalable = {@mayDrawNextFrame, dm, false};
taskList{'control'}{'trial calls'} = trialCalls;

% each trial type gets its own state machine and tree "branch"
%   state machines share many states

%% Fixed Time
fixedMachine = topsStateMachine;
fixedMachine.name = 'traverse states';

prepTime = taskList{'timing'}{'preparation'};
stimTime = taskList{'timing'}{'stimulus'};
choiceTime = taskList{'timing'}{'choice'};
feedTime = taskList{'timing'}{'feedback'};
trialTime = taskList{'timing'}{'trial timeout'};

feedback = {@drawCorrectIncorrect, taskList};
ackLeft = {@acknowledgeLeft, taskList};
ackRight = {@acknowledgeRight, taskList};

fixedStates = { ...
    'name',     'entry',        'timeout',	'input',	'exit',         'next'; ...
    'preparation',{@show,fp},   prepTime,   {},         {},             'stimulus'; ...
    'stimulus', {@show,stim},   stimTime,   {},         {@hide,stim},   'choice'; ...
    'choice',   {@show,targs},  choiceTime, {@queryNextTracked,hid},{},  'feedback'; ...
    'leftward', ackLeft,        0,          {},         {},             'responded'; ...
    'rightward',ackRight,       0,          {},         {},             'responded'; ...
    'responded',{},             0,          {},         {@hide,fp},     'feedback'; ...
    'feedback', feedback,       feedTime,   {},         {@hide,targs},  ''; ...
    'unavailable',{},           0,          {},         {@hide,fp},     'feedback'; ...
    'quit',     {@quitTask},    0,          {},         {},             ''; ...
    };
fixedMachine.addMultipleStates(fixedStates);
fixedMachine.startFevalable = {@startTrial, taskList};
fixedMachine.finishFevalable = {@finishTrial, taskList};
taskList{'control'}{'fixed time machine'} = fixedMachine;

fixedConcurrents = topsConcurrentComposite;
fixedConcurrents.name = 'run() concurrently:';
fixedConcurrents.addChild(dm);
fixedConcurrents.addChild(trialCalls);
fixedConcurrents.addChild(fixedMachine);

% add a branch to the tree trunk to lauch a Fixed Time trial
fixedTree = taskTree.newChildNode;
fixedTree.name = 'fixed time trial:';
fixedTree.iterations = inf;
fixedTree.addChild(taskConditions);
fixedTree.addChild(fixedConcurrents);

%% Reaction Time
reactionMachine = topsStateMachine;
reactionMachine.name = 'traverse states';

% only need to modify states from Fixed Time
%   respond to input during stimulus
%   wait for a button press commitment
reactionStates = { ...
    'name',     'entry',        'timeout',	'input',	'exit',	'next'; ...
    'stimulus', {@show,stim},	trialTime,	{@queryNextTracked,hid},{@hide,stim},   'feedback'; ...
    'responded',{@show,targs},	trialTime,	{@queryNextTracked,hid},{@hide,fp},     'feedback'; ...
    'commitment',{},           	0,          {},         {@hide,fp},     'feedback'; ...
    };

% start with the same states as the Fixed Time trials
%   add and replace some states for reaction time behavior
reactionMachine.addMultipleStates(fixedStates);
reactionMachine.addMultipleStates(reactionStates);
reactionMachine.startFevalable = {@startTrial, taskList};
reactionMachine.finishFevalable = {@finishTrial, taskList};
taskList{'control'}{'reaction time machine'} = reactionMachine;

reactionConcurrents = topsConcurrentComposite;
reactionConcurrents.name = 'run() concurrently:';
reactionConcurrents.addChild(dm);
reactionConcurrents.addChild(trialCalls);
reactionConcurrents.addChild(reactionMachine);

% add a branch to the tree trunk to lauch a Fixed Time trial
reactionTree = taskTree.newChildNode;
reactionTree.name = 'reaction time trial:';
reactionTree.iterations = inf;
reactionTree.addChild(taskConditions);
reactionTree.addChild(reactionConcurrents);


%% Preparation Time
preparationMachine = topsStateMachine;
preparationMachine.name = 'traverse states';

% only need to modify states from Fixed Time
%   respond to input during preparation
preparationStates = { ...
    'name',     'entry',        'timeout',	'input',	'exit',	'next'; ...
    'preparation',{@show,fp},	trialTime,	{@queryNextTracked,hid},{},     'feedback'; ...
    'commitment',{},            0,          {},         {},     'stimulus'; ...
    };

% start with the same states as the Fixed Time trials
%   add and replace some states for preparation time behavior
preparationMachine.addMultipleStates(fixedStates);
preparationMachine.addMultipleStates(preparationStates);
preparationMachine.startFevalable = {@startTrial, taskList};
preparationMachine.finishFevalable = {@finishTrial, taskList};
taskList{'control'}{'preparation time machine'} = preparationMachine;

preparationConcurrents = topsConcurrentComposite;
preparationConcurrents.name = 'run() concurrently:';
preparationConcurrents.addChild(dm);
preparationConcurrents.addChild(trialCalls);
preparationConcurrents.addChild(preparationMachine);

% add a branch to the tree trunk to lauch a Fixed Time trial
preparationTree = taskTree.newChildNode;
preparationTree.name = 'prep. time trial:';
preparationTree.iterations = inf;
preparationTree.addChild(taskConditions);
preparationTree.addChild(preparationConcurrents);

%% Custom Behaviors:
% define behaviors that are unique to this task
%   the control objects above use these
%   the smaller and fewer of these, the better

function startTrial(taskList)
hid = taskList{'input'}{'using'};
hid.flushData;
taskList{'control'}{'current choice'} = 'none';

targs = taskList{'graphics'}{'targets'};
targs.color = taskList{'graphics'}{'gray'};
targs.dotSize = taskList{'graphics'}{'neutral pixels'};


function finishTrial(taskList)
WaitSecs(taskList{'timing'}{'intertrial'});


function acknowledgeLeft(taskList)
taskList{'control'}{'current choice'} = 'leftward';
targs = taskList{'graphics'}{'targets'};
targs.dotSize = taskList{'graphics'}{'leftward pixels'};


function acknowledgeRight(taskList)
taskList{'control'}{'current choice'} = 'rightward';
targs = taskList{'graphics'}{'targets'};
targs.dotSize = taskList{'graphics'}{'rightward pixels'};


function drawCorrectIncorrect(taskList)
stim = taskList{'graphics'}{'stimulus'};
if cos(stim.direction*180/pi) > 0
    stimDir = 'rightward';
else
    stimDir = 'leftward';
end

choiceDir = taskList{'control'}{'current choice'};
targs = taskList{'graphics'}{'targets'};
if strcmp(stimDir, choiceDir)
    targs.color = taskList{'graphics'}{'green'};
else
    targs.color = taskList{'graphics'}{'red'};
end


function quitTask()
dm = dotsTheDrawablesManager.theObject;
dm.mayDrawNextFrame(false);
error('User quit the task')